/*
	PlAr is Platform Arena: a 2D multiplayer shooting game
	Copyright (c) 2010, Antonio Ragagnin <spocchio@gmail.com>
	All rights reserved.

	This file is licensed under the New BSD License.
*/
package plar.ClientServer;

import java.awt.Point;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.util.*; 

import java.net.SocketAddress;
import java.net.SocketException;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import plar.ClientServer.Signal.Info;
import plar.ClientServer.Signal.LevelInfo;
import plar.ClientServer.Signal.Login;

import plar.core.Common;
import plar.core.Game;
import plar.core.KeyFlag;
import plar.core.Level;
import plar.core.ElementPlayer;
import plar.core.SpriteSet;

/**
*
* 1. Server class receive UDP packets and read the ID and Key flag
* 2. if them are both null, create a new ID,Key pair and send it to the Client
*    Create also a new Connect() class to handle packets with this Id,Key pair
*
* data of an UDP packed:
* 1. first byte is the ID
* 2. second byte is the Key
* 3. third byte is the SIGNAL (see Signal.Java)
* 4. the other data could be anything, Connect() read the SIGNAL and decide what kind of data
*    is stored in the packet.
*
* Compressions:
*  for each SpriteSet for each Element in the Level, each SpriteSet is sent only ONE TIME.
*  Repeated SpriteSet are not sent, SpriteSet of an element that is not yet shown is not sent.
*
*  when the client request the content of a string an array of byte is sent, the logical data is:
*  - for each Element in the screen: (int)x,(int)y,(int)angle,(int)SpriteSetId,(int)forFutureUse
*   (forFutureUse it could become the speed in pixel units for local interpolations)
*  - these 4 int, that use almost TEN BITS, are compressed in 5 byte
*
*  the Client send the user input (stored in KeyFlag) once when repeated
*  so the Server, send to Game the last input that the client had sent.
*  for example
*  - Client says the Left key was pressed
*  - Server says to Game that Left Key was pressed until Client don't change it.
*  For this reason is NECESSARY that client send an EMPTY KeyFlag when the user
*  stop pressing keys
*
*  Sprite and Element s:
*  each Element in the Level should use a SpriteSet that contains an Array of Sprite.
*  there are a LOT of elements with equal SpriteSet (usually statics)
*  when the Client request the Screen, Server check if in the screen there are NEW SpriteSet
*  that had not been sent yet,( these SpriteSet are stored in ArrayList <SpriteSet> buffer; )
*  if yes it send the new SpriteSet and add it to "buffer", the Client should do the same
*  and they buffer index should be sincronized.
*  FOR EACH SpriteSet is used an UDP with the Signal.SPRITE
*
*  For each Element in the Screen the server send the coordinates, the angle and the SpriteSetID stored in buffer.
*
* @author Antonio Ragagnin
*
*/

class Connect {
	public Game game;
	private ArrayList<SpriteSet> buffer;
	public byte Key;
	public byte Id;
	DatagramSocket server;
	SocketAddress addr;
	ElementPlayer p;
	ArrayList<ShownElement> screen;
	ArrayList<Integer> indexToSend;
	KeyFlag lastInput;
	String username="noname";
	public long lastSignal=0;
	
	Connect(Game g, DatagramSocket s, byte k,byte id, SocketAddress a)
	throws IOException {
		lastSignal=Calendar.getInstance().getTimeInMillis();;
		Key = k;
		server = s;
		game = g;
		Id=id;
		addr = a;
		Common.info(7, "Connect: delay=");
		buffer = new ArrayList<SpriteSet>();
		screen = new ArrayList<ShownElement>();
		indexToSend = new ArrayList<Integer>();
		lastInput = new KeyFlag((short) 0);
		
	}

	public  synchronized void process(byte[] data, byte signal) throws SocketException,
	IOException, ClassNotFoundException {
		lastSignal=Calendar.getInstance().getTimeInMillis();
		// Common.info(1, "client processing data.length:"+data.length);
		if (signal == Signal.LOGIN) {
			Login login;
			login = (Login) Signal.toObject(data);
			// Common.info(1, "loginclass:"+login);
			String name = login.userName;
			username=name;
			String type = login.playerName;
			if(game.playerList.contains(type))
			type = Common.playersPackage + type;
			else
			type = Common.playersPackage + game.playerList.get(0);

			try {
				@SuppressWarnings("unchecked")
				Class c = Class.forName(type);
				p = (ElementPlayer) c.newInstance();
			} catch (Exception e) {

				Common.info(1, "Connect.process() LOGIN: failed loading " + type);
				close();

			}
			p.username=name;
			game.addPlayer(p);

			Common.info(1, "Connect.run(): new player: " + p + "  name=" + name
			+ " type=" + type);
		}
		if (signal == Signal.WELCOME) {
			LevelInfo li = new LevelInfo();
			Common.info(1,"received a WELCOME!");

			List <String> lp=game.playerList;
			List <String> lg=game.gunList;
			li.MOTD = "Welcome to PlAr server v"+Common.version+" \n";
			li.MOTD += " \n";
			li.MOTD += " The game is running the level \""+game.level.description+"\"\n";
			li.MOTD += " \n";
			li.MOTD += " The are "+game.level.getPlayers().size()+" players online: \n";

			li.MOTD += " \n";
			li.MOTD += " The are "+lg.size()+" aviable guns in the game:\n";
			li.MOTD += Common.joinAL(lg);
			li.MOTD += " \n";
			li.MOTD += " The are "+lg.size()+" aviable players \n";
			li.MOTD += Common.joinAL(lp);
			li.MOTD += " \n";
			li.MOTD += "Please enter the kind of player (case sensitive) \n";

			li.resolution = game.screenResolution;
			li.scaleX = game.scale.x;
			li.scaleY = game.scale.y;
			//li.guns=game.gunList;


			server.send(Signal.send(li, (byte) 0, (byte) 0, Signal.LEVELINFO, addr));
			Common.info(1,"sent a WELCOME!");
		} else if (signal == Signal.SPRITE || signal == Signal.SPRITEANDSCREEN) {
			// Common.info(8,"REQUESTED SPRITES!");

			screen = game.getScreen(p);
			indexToSend = new ArrayList<Integer>();
			ArrayList<Integer> indexToBeAdded = new ArrayList<Integer>();
			for (int i = 0; i < screen.size(); i++) {
				ShownElement e = screen.get(i);
				boolean exist = false;
				for (int j = 0; (j < buffer.size() && !exist); j++) {
					if (e.spriteSet.equals(buffer.get(j))) {
						exist = true;
						// Common.info("found element in position "+j);
						indexToSend.add(j);
					}
				}
				if (!exist) {
					buffer.add(e.spriteSet);
					indexToBeAdded.add(buffer.size() - 1);
					indexToSend.add(buffer.size() - 1);
				}
			}
			for (int i = 0; i < indexToBeAdded.size(); i++) {
				Object o = buffer.get(indexToBeAdded.get(i));
				server.send(Signal.send(o, (byte) 0, (byte) 0, Signal.SPRITE,
				addr));
			}

		} else if (signal == Signal.DIRECTION) {

			byte x = data[0];
			byte y = data[1];
			float dx = (float) x;
			float dy = (float) y;
			dx /= 127;
			dy /= 127;
			Common.info(1, "Connect.run(): someone clicked the mouse: dirx" + dx
			+ " diry" + dy + "");
			p.directionX = dx;
			p.directionY = dy;
		} else if (signal == Signal.MESSAGE) {
			String c = (String) Signal.toObject(data);
			game.sendChat(p, c);
			Common.info(1,"<"+p.username+">" +c);
		} else if (signal == Signal.INFO) {
			if(p==null)
			{
				sendMessage("OPS! Server received signals in the wrong order. (LOGIN,INFO)");
			}else{
			Info info = new Info();
			info.chat = game.getChat();
			info.life = p.energy;
			info.score = p.kills;
			info.spawns=p.killed;
			info.guns=game.getGuns(p);
			info.remaning=game.remaning;
			//info.chat="";
			server.send(Signal
			.send(info, (byte) 0, (byte) 0, Signal.INFO, addr));
			}
		}
		if (signal == Signal.SCREEN || signal == Signal.SPRITEANDSCREEN) {
			// Common.info(8,"REQUESTED SKREEN");
			//int N = 5;
			// int N = 6;
			int N = 4;
			byte[] cData = new byte[screen.size() * N *2];
			// cData[0] = (byte) screen.size();
			// Common.info(8,"screen size:"+screen.size()+" with"+(screen.size()*6));
			for (int i = 0; i < screen.size(); i++) {
				ShownElement s = screen.get(i);
				int Itheta = (((int) (1000 * s.theta)) % 6282);

				float Ptheta = Itheta / 100;
				float Ntheta = Ptheta / 6.2831f;

				int[] by = { s.position.x, s.position.y, Itheta,
					indexToSend.get(i), 0
				};
				
				/*
				byte[] c = Signal.from5IntTo6Bytes(by);
				cData[0 + i * N] = c[0];
				cData[1 + i * N] = c[1];
				cData[2 + i * N] = c[2];
				cData[3 + i * N] = c[3];
				cData[4 + i * N] = c[4];
				*/
				cData[0 + i *2* N] = Signal.From1ShortToFByte((short)s.position.x);
				cData[1 + i *2* N ] = Signal.From1ShortToSByte((short)s.position.x);
				cData[2 + i *2* N] = Signal.From1ShortToFByte((short)s.position.y);
				cData[3 + i *2* N ] = Signal.From1ShortToSByte((short)s.position.y);
				cData[4 + i *2* N] = Signal.From1ShortToFByte((short)Itheta);
				cData[5 + i *2* N ] = Signal.From1ShortToSByte((short)Itheta);
				cData[6 + i *2* N] = Signal.From1ShortToFByte((short)((int)indexToSend.get(i)));
				cData[7 + i *2* N ] = Signal.From1ShortToSByte((short)((int)indexToSend.get(i)));
				//				cData[3 + i * N] = (short)((int)indexToSend.get(i));
				/*	Common.info(1,"sending: "+s.position.x+":["+cData[0 + i * 2*N]+" "+cData[1 + i *2* N]+"]"
										+s.position.y+":["+cData[2 + i *2* N]+" "+cData[3 + i *2* N]+"]"
										+0+":["+cData[4 + i *2* N]+" "+cData[5 + i *2* N]+"]"
										+((int)indexToSend.get(i))+":["+cData[6 + i *2* N]+" "+cData[7 + i *2* N]+"]"
										);
				*/
				//cData[4 + i * N] = c[4];
				// cData[5+i*N]=c[5];

			}
			server.send(Signal.send(cData, (byte) 0, (byte) 0, Signal.SCREEN,
			addr));
		} else if (signal == Signal.INPUT) {
			KeyFlag strc = null;
			try {
				short key = (Short) Signal.toObject(data);
				strc = new KeyFlag(key);
			} catch (Exception e) {
				strc = lastInput;
			}

			lastInput = strc;
			game.sendInput(p, strc);
		}


		if (signal == Signal.STOP) {
			close();

		}
	}
	public  synchronized  void sendMessage(String s)
	{
		try{
			server.send(Signal
			.send(s, (byte) 0, (byte) 0, Signal.MESSAGE, addr));
		}catch(Exception e)
		{
			Common.info(1," ERROR sendin message "+s+" to "+Id);
		}
	}
	public  synchronized  void close() {
		game.sendChat( username + " has quit the game!");
		if(p!=null)		game.level.delElement(p);
	}

}

public class Server extends Thread {
	public boolean active = true;
	public DatagramSocket dateServer;
	public Game g;
	public Level level;
	public HashMap<String, ElementPlayer> playerList;
	public HashMap<Byte, Connect> connections;
	public long refresh = 10;
	public long timeout = 60*1000;
	public Point resolution;
	public long duration;
	public long now;
	public long start;
	

	public Server() {
		super();
		resolution = new Point(700, 700);
		connections = new HashMap<Byte, Connect>();
		duration=60*1000*(new Long(Common.getConfig().getProperty("time")));
		now=Common.now();
		start=Common.now();
	}

	
	public void close() {
		active = false;
		for (Byte b : connections.keySet()) {
			connections.get(b).close();

		}
		dateServer.close();
		this.interrupt();

	}

	public boolean listen(int port) {
		try {

			dateServer = new DatagramSocket(port);

			System.out.println("Server listening on port " + port + ".");
			this.start();
		} catch (Exception e) {
			System.out.println("failed listeninng.");
			return false;
		}
		return true;
	}

	public void newGame(Level l) {
		g = new Game();
		g.screenResolution = resolution;

		g.setLevel(l);
		g.startGame();
	}
	public String getScore()
	{
		Collection cc = connections.values(); 
		String finale="<html><table><tr><td>Username</td><td>Score</td><td>Deaths</td>";
		{
			Iterator itr = cc.iterator();
			
			while(itr.hasNext())
			{
				
				Connect c = (Connect)itr.next();
				if(c!=null){
					finale+="<tr><td>["+c.p.username+"]</td><td> "+(c.p.kills)+"</td><td> "+(c.p.killed)+"</td></tr>";
					
				}
			}
			finale+="</html>";
		}
		return finale;
		
	}
	public void sendBroadcast(String finale){
		
		Collection cc = connections.values(); 
		Iterator itr = cc.iterator();
		while(itr.hasNext())
		{
			
			Connect c = (Connect)itr.next();
			if(c!=null){
				c.sendMessage(finale);
				
			}
		}
		
	}
	public void restartGame()
	{
		String msg="Game Finished!\n\nHighscores:\n"+getScore();
		
		sendBroadcast(msg);				 
		
		g.restart();
		start=Common.now();
		
	}
	public void loadGame() {
		class autoupdate extends Thread {

			public void run() {

				try {
					while (active) {
						
						g.run();
						Thread.sleep(refresh);
					}
				} catch (Exception e) {
					Common.info(1," --------------------------------------- ");
					e.printStackTrace();
				}
			}
		}
		
		
		
		
		class FindTimeout extends Thread {

			public void run() {

				
				try {
					while (active) {
						Thread.sleep(timeout);
						syncrun();
						
						
					}
				}					catch (InterruptedException e) {

					e.printStackTrace();
				}
			}
			
			public void syncrun()
			{
				synchronized (connections)
				{
					
					Collection cc = connections.values(); 
					Iterator itr = cc.iterator();

					//iterate through HashMap values iterator
					while(itr.hasNext())
					{
						
						Connect c = (Connect)itr.next();
						if(c!=null){
							
							
							if((c.lastSignal+timeout)<now) {
								Common.info(1,"FOUND GHOST!");
								c.close();
								connections.remove(c.Id);
							}
						}else Common.info(1,"* c="+c);
					}  
				}
				
				
			}
			
		}

		class RestartMatch extends Thread {

			public void run() {
				try {
					
					while (active) {
						
						Thread.sleep(duration);
						syncrun();
					}
				}catch (InterruptedException e) {

					e.printStackTrace();
				}
				
				
			}
			
			
			
			
			public void syncrun()
			{
				
				synchronized(connections)
				{
					restartGame();
					
				}
				
				
			}
		}
		Thread t1 = new autoupdate();
		t1.setName("Thread principale");
		t1.setPriority(10);
		t1.start();
		
		Thread t2 = new FindTimeout();
		t2.setName("Thread ghost");
		t2.setPriority(10);
		t2.start();
		
		Thread t3 = new RestartMatch();
		t3.setName("Thread mathes");
		t3.setPriority(10);
		t3.start();

		Common.info(1, "Server.newGame() refreshtime=" + refresh + " level="
		+ level);
	}

	public void run() {
		try {
			while (active) {
				syncrun();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}

	}
	public void syncrun() throws Exception
	{
		
		now=Common.now();
		g.remaning=start+duration-now;
		byte[] data = new byte[dateServer.getSendBufferSize()];
		DatagramPacket dp = new DatagramPacket(data, 2048);
		// System.out.println("Waiting for datagrams");
		dateServer.receive(dp);
		data = Arrays.copyOf(data, dp.getLength());
		// System.out.println("Waiting for datagrams"+dp.getData().length);
		// = dp.getData();
		if (data.length >= 2) {
			byte ID = Signal.getID(data);
			byte Key = Signal.getKey(data);
			// Common.info(1,
			// "received a packet. ID="+ID+" KEY="+Key+" from:"+dp.getSocketAddress());
			synchronized (connections){
				if (connections.containsKey(ID)
						&& connections.get(ID).Key == Key) {
					// Common.info(1, "it matches in the database");
					byte sig = Signal.getSignal(data);
					// Common.info(1, "signal = "+sig);
					byte[] newData = Signal.getData(data);
					connections.get(ID).process(newData, sig);
				} else if (Key == 0) {

					// log the new client
					byte newID = (byte) (connections.size() + 1);
					byte newKey = (byte) (connections.size());
					Common.info(1, "created new connection:  ID=" + newID
					+ " KEY=" + newKey);

					dateServer.send(Signal.send(new byte[0], newID, newKey,
					(byte) 0, dp.getSocketAddress()));
					Connect c = new Connect(g, dateServer, newKey,newID, dp
					.getSocketAddress());
					connections.put(newID, c);
				}
			}
		}
		
	}

}
